/**
 * 基于Backbone.View的基础视图，增加了父子视图，数据模型绑定等功能，一个视图可以作为一个父视图添加
 * 子视图，子视图也可以通过parentView来引用父视图。
 * BackboneView v1.0.3
 * @author jiangxingshang
 * @date 2015/11/12
 */

 (function(factory) {

   // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
   // We use `self` instead of `window` for `WebWorker` support.
   var root = (typeof self == 'object' && self.self === self && self) ||
     (typeof global == 'object' && global.global === global && global);

   // Set up Backbone appropriately for the environment. Start with AMD.
   if (typeof define === 'function' && define.amd) {
     define(['backbone', 'jquery', 'underscore'], function(Backbone, $, _) {
       // Export global even in AMD case in case this script is loaded with
       // others that may still expect a global Backbone.
       return factory(Backbone, $, _);
     });

     // Next for Node.js or CommonJS. jQuery may not be needed as a module.
   } else if (typeof exports !== 'undefined') {
     var Backbone = require('backbone');
     var _ = require('underscore'), $;
     try { $ = require('jquery'); } catch (e) {}

     module.exports = factory(Backbone, $, _);

     // Finally, as a browser global.
   } else {
     root.BackboneView = factory(root.Backbone, (root.jQuery || root.Zepto || root.ender || root.$), root._);
   }

 })(function(Backbone, $, _) {


   //////////////////////////////////////////////////////
   // 键值映射模拟
   //////////////////////////////////////////////////////
   function Map() {
     this._data = {};
   }

   Map.prototype.set = function(key, value) {
     this._data[key] = value;
     return this;
   };

   Map.prototype.get = function(key) {
     return this._data[key] || null;
   };

   Map.prototype.delete = function(key) {
     delete this._data[key];
     return this;
   };

   Map.prototype.clear = function() {
     this._data = {};
     return this;
   };

   Map.prototype.has = function(key) {
     var tmp = this._data[key];
     return tmp !== null && tmp !== undefined;
   };

   Map.prototype.keys = function() {
     var arr = [];
     for(var p in this._data) {
       if(this._data.hasOwnProperty(p)) {
         arr.push(p);
       }
     }
     return arr;
   };

   Map.prototype.values = function() {
     var arr = [];
     for(var p in this._data) {
       if(this._data.hasOwnProperty(p)) {
         arr.push(this._data[p]);
       }
     }
     return arr;
   };

   Map.prototype.forEach = function(callback) {
     for(var p in this._data) {
       if(this._data.hasOwnProperty(p)) {
         callback.call(this, p, this._data[p]);
       }
     }
   };

   Map.prototype.size = function() {
     var i = 0;
     for(var p in this._data) {
       if(this._data.hasOwnProperty(p)) {
         i++;
       }
     }
     return i;
   };

   //随机ID
   function makeViewId(len) {
     len = len || 5;
     var chars = '123456789abcdefghijklmnopqrstuvwxyz'.split('');
     var uuid = [], i;
     var radix = 16;
     if (len) {
       for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
     } else {
       var r;
       uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
       uuid[14] = '4';
       for (i = 0; i < 36; i++) {
         if (!uuid[i]) {
           r = 0 | Math.random()*16;
           uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
         }
       }
     }

     return uuid.join('');
   }


   //////////////////////////////////////////////////////
   // 主应用
   //////////////////////////////////////////////////////
   var BaseView = Backbone.View.extend({

     /**
      * 获取用于添加子View的容器，默认为$el。
      */
     getViewContainer: function() {
       return this.$el;
     },

     /**
      * 视图ID，当视图被添加到父视图中时，自动会在$el元素上设置一个view-id属性，这个属性值就是getViewId()，
      * viewId用于在父视图下查找DOM元素。默认返回视图的Model.id，如果没有Model则随机产生一个id，你可以覆盖
      * 这个函数来自定义一个viewId，但要注意，一旦id被设置了就不能再改变，也就是getViewId()返回的应永远一样。
      * @returns {string}
      */
     getViewId: function() {
       var id;
       if(this.$el) {
         id = BaseView.processViewId(this.$el);
       }
       return id || (this.model ? this.model.id : makeViewId());
     },

     /**
      * 添加子视图，子视图会被设置一个parentView来引用当前父视图，view被触发一个add事件。
      * @param view
      * @param [index] {int | string | function}
      *    提供一个数字（从0开始）来插入到指定的位置，'append'或'prepend'来添加到容器的顶部或底部，
      *    函数可以自己处理插入位置，参数是view，如果不提供index则添加到容器底部。
      * @param [pos] {String} 当index是一个数字类型时，pos的可选值为before或after，表示插入到
      *    index所在元素的前面还是后面（如果index存在一个元素，否则直接插入到父视图最后。）
      */
     addView: function(view, index, pos) {
       if(view.getViewId == undefined) throw new Error('view is not extend from backbone-view.');
       if(view.parentView) throw new Error('view already has parent view.');
       if(this.views == undefined || this.views == null) this.views = new Map();
       var id = view.getViewId();
       var $e = view.$el.attr('view-id', id);
       view.parentView = this;
       var vc = this.getViewContainer();
       if(_['isFunction'](index)) {
         index.call(this, view)
       } else if(_['isNumber'](index)) {
         var child = vc.children(':eq('+ index + ')');
         if(child.length > 0) {
           $e[pos == 'before' ? 'insertBefore' : 'insertAfter'](child);
         } else {
           vc.append($e);
         }
       } else if(_['isString'](index)) {
         vc[index]($e);
       } else {
         vc.append($e);
       }
       this.views.set(id, view);
       view.trigger('add');
       this.trigger('addView', view, index);
       return this;
     },

     /**
      * 返回子视图。
      * @param viewId
      */
     getView: function(viewId) {
       return this.views ? this.views.get(viewId) : null;
     },

     get: function() {
       return this.$el;
     },

     getParent: function() {
       return this.parentView;
     },

     firstView: function() {
       var e = this.getViewContainer().children(':first');
       return this.getView(BaseView.processViewId(e));
     },

     lastView: function() {
       var e = this.getViewContainer().children(':last');
       return this.getView(BaseView.processViewId(e));
     },

     prevView: function() {
       if(this.parentView) {
         return this.parentView.getView(BaseView.processViewId(this.$el.prev()));
       }
       return null;
     },

     nextView: function() {
       if(this.parentView) {
         return this.parentView.getView(BaseView.processViewId(this.$el.next()));
       }
       return null;
     },

     setZIndex: function(index) {
       this.zIndex = index;
       this.$el.css('z-index', index);
       return this;
     },

     getZIndex: function() {
       return this.zIndex || 0;
     },

     /**
      * 将本视图放到目标视图的后面。
      * @param targetView @{BaseView}
      */
     afterOf: function(targetView) {
       targetView.$el.after(this.$el);
       return this;
     },

     /**
      * 将本视图放到目标视图的前面。
      * @param targetView @{BaseView}
      */
     beforeOf: function(targetView) {
       targetView.$el.before(this.$el);
       return this;
     },

     /**
      * 获取子视图的数量。
      * @returns {*}
      */
     getViewCount: function() {
       return this.views ? this.views.size() : 0;
     },

     /**
      * 排序子视图，注意是排序DOM的元素位置，而不是子视图在父视图中的位置。
      * @param property {string | function} 作为排序条件的属性名，如果是一个函数，你从函数中可以得到子视图对象，并且你要返回一个值用于排序。
      * @param desc {string} 不提供则升序，提供'desc'则降序。
      */
     sort: function(property, desc) {
       var arr = this.subViews();
       if(arr.length <= 1) return;
       var tmp = [];
       //复制必要的属性到临时对象，避免underscore复制我们的视图对象
       var func = function(subView) {
         return subView[property];
       };
       if(typeof property == 'function') {
         func = property;
         property = 'prop';
       }
       for(var i = 0; i < arr.length; i++) {
         var obj = {id: arr[i].getViewId()};
         obj[property] = func(arr[i]);
         tmp.push(obj);
       }
       tmp = _.sortBy(tmp, property);
       if(desc === 'desc') {
         tmp = tmp.reverse();
       }

       //整理DOM的顺序
       var c = this.getViewContainer();
       var firstView = this.getView(tmp[0].id);
       c.prepend(firstView.$el);
       for(i = 1; i < tmp.length; i++) {
         var view = this.getView(tmp[i].id);
         if(firstView.nextView() != view) {
           view.afterOf(firstView);
         }
         firstView = view;
       }
       return this;
     },

     /**
      * 遍历子视图。
      * @param cb 第一个参数是视图对象，第二个是视图在文档结构里的位置。
      */
     each: function(cb) {
       var self = this;
       var views = this.views;
       if(views == null || views == undefined) return;
       this.getViewContainer().children('[view-id]').each(function(i) {
         var id = BaseView.processViewId($(this));
         var view = views.get(id);
         return cb.call(self, view, i);
       });
       return this;
     },

     /**
      * 查找符合条件的子视图。
      * @param properties 条件，满足这些属性值的视图才会返回，如{age: 16}，那么所有属性age为16的子视图就会返回。
      * @param [one] {boolean} true表示只返回一个（视图），忽略其他，其他值返回全部满足条件的（数组）。
      */
     findView: function(properties, one) {
       var tmp = one === true ? null : [];
       this.each(function(view) {
         var f = true;
         for(var p in properties) {
           if(properties[p] !== view[p]) {
             f = false;
             break;
           }
         }
         if(f == false) return true;//continue to next;
         if(one === true) {
           tmp = view;
           return false;//break iterator
         } else {
           tmp.push(view);
         }
       });
       return tmp;
     },

     /**
      * 返回子视图数组。
      * @returns {Array}
      */
     subViews: function() {
       var arr = [];
       this.each(function(view) {
         arr.push(view);
       });
       return arr;
     },

     /**
      * 移除view并返回这个view。
      * @param viewId
      * @param [f] {boolean} 参考remove(f)
      * @returns {*}
      */
     removeView: function(viewId, f) {
       var view = this.getView(viewId);
       if(view != null) {
         view.remove(f);
       }
       return view;
     },

     /**
      * 将所有子视图都删除。
      */
     empty: function() {
       this.each(function(view) {
         this.removeView(view.getViewId())
       });
       return this;
     },

     /**
      * 删除视图本身，从父视图中脱离，从DOM上移除元素，解绑Model。
      * 触发remove()事件，如果视图存在父视图的话，父视图将会触发removeView(subView)事件。
      * @param f {boolean} 明确提供false来让父子视图脱离关系，而不会使视图本身的html元素被移除，事件也仍会保留。
      */
     remove: function(f) {
       this.trigger('beforeRemove');
       this.onBeforeRemoveSelf && this.onBeforeRemoveSelf();
       if(this.parentView && this.parentView.views) {
         this.parentView.views.delete(this.getViewId());
       }
       if(f !== false) {
         this._removeElement();
         this.stopListening();
         this.unbindModel();
         this.empty();
       }
       this.onRemoveSelf && this.onRemoveSelf();
       this.trigger('remove');
       if(this.parentView) {
         this.parentView.trigger('removeView', this);
       }
       this.parentView = null;
       viewCache.delete(this.getViewId());
       return this;
     },

     /**
      * 将Model和View绑定，当Model的属性发生改变时可以立即更新View的数据。
      * 你必须先初始化$el才能调用此函数，在删除视图之前你必须先unbindModel。
      * 当你绑定Model后，后面要从View中剥离Model换更换前一定要unbindModel。bindModel函数已经先为你做了unbindModel。
      *
      * html结构：
      * 你必须使用一个容器来包裹着含有data-binding属性的元素。
      * <div>
      *   <span data-binding="title"></span>
      *   <span data-binding="{property:'title'}"></span>
      * </div>
      *
      * config：
      * {
      *   property: {string} 绑定到Model的属性。
      *   valueType: {string} 设置值到视图上的方式，支持'text'，'val'，'attr'，'html'，分别对应jQuery的方法，默认为'text'。
      *   method: {string} Model的函数名，如果提供，则通过model[method]来获取值，否则使用model.get(config.property)来获取值。
      *   args: {Array} 传给method的参数，method.call(model, args)。
      *   prefix: {string} 如果提供，则会加在从Model获取到的值的前面。
      *   suffix: {string} 如果提供，则会加在从Model获取到的值的后面。
      *   attrKey: {string} 如果valueType='attr'，那么从model获取到的值将渲染在视图的这个属性上。
      * }
      *
      * 例子：
      * <span data-binding="title"></span>
      * 这是最简单的，当Model的title属性发生变化时，$(span).text(model.get('title'))。
      *
      * <img class="cover" data-binding="{property:'coverUrl', method:'getCoverUrl', args:['2x'], valueType:'attr', attrKey:'src'}">
      * 当Model的coverUrl属性发生变化时，调用Model的getCoverUrl('2x')函数获取到值，然后将值设置到img的src属性上。
      *
      * JS：
      * BaseView.extend({
 	   *    initialize: function() {
 		 *      this.$el = ...;//初始化$el
 		 *      var bindOptions = {}
 		 *      this.bindModel(bindOptions);
 	   *    }
      * });
      *
      * options：
      * {
      *   model: {Model} 数据模型，默认为视图已有的Model。
      *   render: {boolean} 是否立即渲染模型的数据到视图上，默认为true。
      * }
      */
     bindModel: function(options) {
       this.unbindModel();

       options = $.extend({
         model: this.model,
         render: true
       }, options);

       var model = this.model = options.model;
       if(model == null || model == undefined) return;

       var context = this;

       function setData(view, model, config, property) {
         var func = model[config.method];
         var value;
         if($.isFunction(func)) {
           if($.isArray(config['args'])) {
             value = func.call(model, config['args'])
           } else {
             value = model[config.method]()
           }
         } else {
           value = model.get(property)
         }

         if((value == null || value == undefined) || typeof value == 'string' && value.length == 0) {
           value = '';
         } else {
           value = config.prefix + value + config.suffix;
         }

         switch(config.valueType) {
           case 'text':
             view.text(value);
             break;
           case 'val':
             view.val(value);
             break;
           case 'attr':
             view.attr(config['attrKey'], value);
             break;
           case 'html':
             view.html(value);
             break;
           case 'style':
             view.css(config['styleKey'], value);
             break;
         }
       }

       this.$('[data-binding]').each(function() {
         var item = $(this);

         var configString = item.attr('data-binding');
         if(configString.indexOf('{') == -1) {
           configString = '{property: "' + configString + '"}'
         }

         var config = $.extend({
           property: '_not_found',
           valueType: 'text',
           prefix: '',
           suffix: ''
         }, eval('(' + configString + ')'));


         if(config.property == '_not_found') return;

         if(!$.isArray(config.property)) {
           config.property = [config.property];
         }

         config.property.forEach(function(name) {
           model.on('change:' + name, function(model) {
             setData(item, model, config, name)
           }, context);
         });

         if(options.render !== false) {
           setData(item, model, config, config.property[0])
         }

         item.removeAttr('data-biding');
       });
       return this;
     },

     /**
      * 监听视图的Model的属性变化，当你调用unbindModel时会停止监听器。
      * @param property
      * @param cb {Function} 参数1是Model，参数2是新值。
      * @param [trigger] {boolean} 是否立即触发cb执行一次，默认为true。
      */
     bindModelProp: function(property, cb, trigger) {
       if(this.model) {
         this.model.on('change:' + property, cb, this);
         if(trigger !== false) {
           cb.call(this, this.model, this.model.get(property));
         }
       }
     },

     /**
      * 移除对Model的属性绑定，不再监听属性变化。
      */
     unbindModel: function() {
       if(this.model) {
         this.model.off(null, null, this);
       }
       return this;
     },

     /**
      * 监听Backbone.Collection的add和remove操作，当集合发生添加操作时，自动添加一个ViewClass到本容器中，
      * 集合发生移除操作时也会自动将该子视图从本视图中删除。
      * @param collection
      * @param ViewClass {function} 子视图类，使用new ViewClass来创建，该视图的viewId必须是Model.id。
      * @param options
      *   add: {boolean | function} 如果不想监听集合的add事件，则设置为false，默认为true，如果设置为一个函数，
      *                             则添加视图后会执行该函数，参数1是ViewClass实例，参数2是Collection，参数3是Model。
      *   addPosition {Object} 该参数会传给addView()的第二个参数，优先使用collection#add事件中的options.at，然后是Model本身的addPosition属性，最后才会用这个配置项。
      *   remove: {boolean | function} 和add配置的描述一样，监听的是集合的remove事件。
      *   filter: {function} 集合发生add或remove事件时，会先执行该函数，如果你要终止后面的操作（添加或移除视图）则需要明确的返回false。
      *                      参数1是一个Model，参数2是操作类型（'add'或'remove'）。
      *   data: {object} 传给子视图构造函数的配置参数。
      */
     bindCollection: function(collection, ViewClass, options) {
       if(ViewClass['processViewId'] == undefined) throw new Error('ViewClass must extend from BaseView.');
       options = $.extend({
         add: true,
         addPosition: 'append',
         remove: true,
         filter: function(model, action) {
           return true;
         },
         data: null
       }, options);
       if(options.add !== false) {
         this.listenTo(collection, 'add', function(model, coll, collOpt) {
           if(options.filter.call(this, model, 'add') === false) return;
           var opt = options.data || {};
           opt.model = model;
           var view = new ViewClass(opt);
           var addPos = collOpt.at;
           if(addPos == undefined) addPos = model.addPosition;
           if(addPos == undefined) addPos = options.addPosition;

           this.addView(view, addPos, addPos === 0 ? 'before' : 'after');
           _.isFunction(options.add) && options.add.call(this, view, collection, model);
         })
       }

       if(options.remove !== false) {
         this.listenTo(collection, 'remove', function(model) {
           if(options.filter.call(this, model, 'remove') === false) return;
           var view = this.removeView(model.id);
           _.isFunction(options.remove) && options.remove.call(this, view, collection, model);
         })
       }
     },

     unbindCollection: function(collection) {
       this.stopListening(collection);
     },

     /**
      * 使用配置的方式为对象绑定事件，主要是为了解决Backbone的events配置问题，如果$el是在initialize中初始化的话，events配置就无意义了。
      * options配置规则：
      * {
      *   'property': { 'event1': Callback, 'event2': Callback },
      *   '$ selector': { 'event1': Callback, 'event2': Callback },
      *   'event property': Callback,
      *   'event $ selector': Callback
      * }
      *
      * Callback可以是一个字符串或是一个函数，如果是字符串，则表示当前View的方法名，如果是一个函数，其this会被指向为当前View。
      * property表示当前View的属性，这个属性只能是一个jQuery对象或是Backbone.View。
      * $是固定值，如果加了$，那么它的后面的selector就是一个查询器，会被这样this.$(selector)用来查询jQuery元素。
      * event，event1，event2，eventX是事件名。
      *
      * 例子：
      * var view = this;
      * view.bindEvents({
      *   'uploadView': {
      *     'start': 'onUploadViewStart',
      *     'stop': function() {
      *       //this == view
      *     },
      *     'complete': function() {}
      *   },
      *
      *   '$ button.upload': {
      *     'click': 'onUploadClick'
      *   },
      *
      *   'upload uploadView': 'onUploadViewUpload',
      *
      *   'click $ button.stop': 'onStop'
      * })
      */
     bindEvents: function(options, context, selectorContext) {
       var context = context || this;
       for(var p in options) {
         if(!_.has(options, p)) continue;

         var r = this._processEventConfig(p, options[p], selectorContext);
         var target = r.target;
         var events = r.events;
         _.mapObject(events, function(callback, event) {
           var method = _['isString'](callback) ? context[callback] : callback;
           target.on(event, function() {
             method.apply(context, arguments)
           });
         })
       }
     },

     /**
      * 将本视图缓存到内存中，你可以通过View类在任何地方通过视图id获取到视图本身。
      * @since v2.0.3
      * @return {BaseView}
      */
     cache: function() {
       viewCache.set(this.getViewId(), this);
       return this;
     },

     /**
      * 渲染视图数据，和bindModel一样，但此函数不监听数据变化。
      * @since 2.0.3
      * @param options
      * @return {BaseView}
      */
     renderModel: function(options) {

       options = $.extend({
         model: this.model,
         render: true
       }, options);

       var model = this.model = options.model;
       if(model == null || model == undefined) return;

       function setData(view, model, config, property) {
         var func = model[config.method];
         var value;
         if($.isFunction(func)) {
           if($.isArray(config['args'])) {
             value = func.call(model, config['args'])
           } else {
             value = model[config.method]()
           }
         } else {
           value = model.get(property)
         }

         if((value == null || value == undefined) || typeof value == 'string' && value.length == 0) {
           value = '';
         } else {
           value = config.prefix + value + config.suffix;
         }

         switch(config.valueType) {
           case 'text':
             view.text(value);
             break;
           case 'val':
             view.val(value);
             break;
           case 'attr':
             view.attr(config['attrKey'], value);
             break;
           case 'html':
             view.html(value);
             break;
           case 'style':
             view.css(config['styleKey'], value);
             break;
         }
       }

       this.$('[data-render]').each(function() {
         var item = $(this);

         var configString = item.attr('data-render');
         if(configString.indexOf('{') == -1) {
           configString = '{property: "' + configString + '"}'
         }

         var config = $.extend({
           property: '_not_found',
           valueType: 'text',
           prefix: '',
           suffix: ''
         }, eval('(' + configString + ')'));


         if(config.property == '_not_found') return;

         if(!$.isArray(config.property)) {
           config.property = [config.property];
         }

         setData(item, model, config, config.property[0]);

         item.removeAttr('data-render');
       });
       return this;
     },


     /**
      * @since 2.0.3
      * @return {BaseView}
      */
     show: function() {
       this.get().show();
       return this;
     },

     /**
      * @since 2.0.3
      * @return {BaseView}
      */
     hide: function() {
       this.get().hide();
       return this;
     },

     /**
      * 与$函数相同，本函数增加了占位符，如：
      * view.jq('.who[data="{0}"]', 'abc');
      *
      * {n}是一个占位符，里面的数字和后面的参数位置对应。
      *
      * @since 2.0.3
      * @param selector
      * @return {*}
      */
     jq: function(selector) {
       if(selector == null || selector == undefined) return '';
       for ( var i = 0; i < arguments.length - 1; i++) {
         selector = selector.replace("{" + i + "}", arguments[i + 1]);
       }
       return this.get().find(selector);
     },

     _processEventConfig: function(key, value, selectorContext) {
       var tmp = key.split(' ');

       //左边的配置
       var eventName, selector, property;
       //右边的配置，config是一个object配置对象，callback是一个回调函数，methodName是一个函数名
       var config;

       if(key.indexOf('$') == 0) {// $ 选择器: {事件a:, 事件b}
         selector = key.substr(2);
         config = value;
       } else if(tmp.length == 1) {// 属性: {事件a:, 事件b}
         property = key;
         config = value;
       } else if(tmp.length == 2) {// 事件名 属性
         eventName = tmp[0];
         property = tmp[1];
       } else if(tmp.length >= 3) {// 事件名 $ 选择器
         eventName = tmp[0];
         selector = '';
         for(var j = 2; j < tmp.length; j++) {
           selector += tmp[j] + (j == tmp.length - 1 ? '' : ' ')
         }
       }

       var eventObj = selector ? ( selectorContext ? selectorContext.find(selector) : this.$(selector) ) : this[property];
       var events = {};
       if(config) {
         events = config
       } else {
         events[eventName] = value
       }

       return {
         target: eventObj,
         events: events
       }
     }

   });

   BaseView.processViewId = function(e) {
     return $(e).attr('view-id');
   };

   var viewCache = new Map;

   /**
    * @since v2.0.3
    * @param id
    */
   BaseView.findView = function(id) {
     return viewCache.get(id);
   };


   return BaseView;
 });